﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Linq;
using System.Text;

namespace NeuralNetworks
{
    public class NeuralNetwork
    {
        #region Constructors

        public NeuralNetwork(NeuralNetworkDefinition definition)
        {
            _definition = definition;

            this.CreateLayers();
        }

        #endregion

        #region Private Members

        private NeuralNetworkDefinition _definition;
        private NeuronsLayer _inputLayer;
        private NeuronsLayer _outputLayer;
        private NeuronsLayer[] _hiddenLayers;

        #endregion

        #region Attributes and Properties

        private float _higherErrorFactor = 0f;
        public float HigherErrorFactor
        {
            get { return _higherErrorFactor; }
            set { _higherErrorFactor = value; }
        }

        private float _lastHigherError = 0f;
        public float LastHigherError
        {
            get { return _lastHigherError; }
        }

        #endregion

        #region Private Methods

        private void CreateLayers()
        {
            _outputLayer = new NeuronsLayer(_definition.OutputLayerNeuronsCount, null);

            _hiddenLayers = new NeuronsLayer[_definition.HiddenLayersCount];
            var lastLayer = _outputLayer;
            _hiddenLayers.ForReversed((hiddenLayer, i) =>
            {
                _hiddenLayers[i] = new NeuronsLayer(_definition.HiddenLayersNeuronsCount, lastLayer);
                lastLayer = _hiddenLayers[i];
            });

            _inputLayer = new NeuronsLayer(_definition.InputLayerNeuronsCount, _hiddenLayers.First());
            foreach (var neuron in _inputLayer.Neurons)
                neuron.FirstLayer = true;
        }

        private short[] CalculateValues(short[] values)
        {
            _inputLayer.Neurons.For((neuron, i) =>
                neuron.Add(null, values[i]));

            _inputLayer.Propagate();

            foreach (NeuronsLayer hiddenLayer in _hiddenLayers)
                hiddenLayer.Propagate();

            _outputLayer.Propagate();

            short[] result = new short[_outputLayer.Neurons.Count()];
            _outputLayer.Neurons.For((neuron, i) =>
                result[i] = (short)Math.Round(neuron.Value));

            return result;
        }

        private void CalculateErrors(short[] expectedValues)
        {
            _outputLayer.Neurons.For((neuron, i) =>
                neuron.ExpectedValue = expectedValues[i]);

            _outputLayer.CalculateErrors();

            foreach (NeuronsLayer hiddenLayer in _hiddenLayers)
                hiddenLayer.CalculateErrors();

            _inputLayer.CalculateErrors();
        }

        private void UpdateWeights()
        {
            foreach (NeuronsLayer hiddenLayer in _hiddenLayers)
                hiddenLayer.UpdateWeights();

            _inputLayer.UpdateWeights();
        }

        #endregion

        #region Public Methods

        public short[] Calculate(short[] values)
        {
            if (values.Count() != _definition.InputLayerNeuronsCount)
                throw new Exception("O número de elementos de entrada deve ser igual ao número de neurônios da camada de entrada.");

            return this.CalculateValues(values);
        }

        public short[] Train(short[] values, short[] expectedValues)
        {
            if (expectedValues.Count() != _definition.OutputLayerNeuronsCount)
                throw new Exception("O número de elementos de resultado esperado deve ser igual ao número de neurônios da camada de saída.");

            short[] result = this.Calculate(values);
            this.CalculateErrors(expectedValues);
            this.UpdateWeights();

            _lastHigherError = 0f;
            _outputLayer.Neurons.For((neuron, i) =>
                {
                    float diff = Math.Abs(expectedValues[i] - neuron.Value);

                    if (_lastHigherError < diff)
                        _lastHigherError = diff;

                    if (diff > _higherErrorFactor)
                        _higherErrorFactor = diff;
                });

            return result;
        }

        public void ResetHigherErrorFactor()
        {
            _higherErrorFactor = 0f;
        }

        public void WriteToConsole()
        {
            Console.WriteLine("-----------------------------------------------------------------------------------------------------");

            Console.WriteLine("INPUT LAYER:");
            for (int i = 0; i < _inputLayer.Neurons.Count(); i++)
            {
                Console.WriteLine(string.Format("N[{0}]: V:{1} E:{2}", i, _inputLayer.Neurons[i].Value, _inputLayer.Neurons[i].Error));
                for (int j = 0; j < _inputLayer.Neurons[i].Connections.Count(); j++)
                    Console.WriteLine(string.Format("   C[{0}]: W:{1}", j, _inputLayer.Neurons[i].Connections[j].Weight));
            }
            Console.WriteLine();

            for (int i = 0; i < _hiddenLayers.Count(); i++)
            {
                Console.WriteLine(string.Format("HIDDEN LAYER[{0}]:", i));
                for (int j = 0; j < _hiddenLayers[i].Neurons.Count(); j++)
                {
                    Console.WriteLine(string.Format("N[{0}]: V:{1} E:{2}", j, _hiddenLayers[i].Neurons[j].Value, _hiddenLayers[i].Neurons[j].Error));
                    for (int k = 0; k < _hiddenLayers[i].Neurons[j].Connections.Count(); k++)
                        Console.WriteLine(string.Format("   C[{0}]: W:{1}", j, _hiddenLayers[i].Neurons[j].Connections[k].Weight));
                }

                Console.WriteLine();
            }

            Console.WriteLine("OUTPUT LAYER:");
            for (int i = 0; i < _outputLayer.Neurons.Count(); i++)
                Console.WriteLine(string.Format("N[{0}]: V:{1} E:{2}", i, _outputLayer.Neurons[i].Value, _outputLayer.Neurons[i].Error));
        }

        public IEnumerable<string> ToSerializableText()
        {
            List<string> lines = new List<string>();
            lines.Add(string.Format("HLC:{0}", _definition.HiddenLayersCount));
            lines.Add(string.Format("ILNC:{0}", _definition.InputLayerNeuronsCount));
            lines.Add(string.Format("HLNC:{0}", _definition.HiddenLayersNeuronsCount));
            lines.Add(string.Format("OLNC:{0}", _definition.OutputLayerNeuronsCount));
            lines.Add("IL:");
            _inputLayer.Neurons.For((neuron, i) =>
                {
                    neuron.Connections.For((conn, j) =>
                        {
                            lines.Add(string.Format("N[{0}][{1}]:{2}", i, j, conn.Weight));
                        });
                });
            _hiddenLayers.For((layer, i) =>
                {
                    lines.Add(string.Format("HL[{0}]:", i));
                    layer.Neurons.For((neuron, j) =>
                        {
                            neuron.Connections.For((conn, k) =>
                                {
                                    lines.Add(string.Format("N[{0}][{1}]:{2}", j, k, conn.Weight));
                                });
                        });
                });

            return lines;
        }

        public void Load(string[] lines)
        {
            _definition = new NeuralNetworkDefinition();
            _definition.HiddenLayersCount = (int)this.GetFloatAfter(lines[0], "HLC:");
            _definition.InputLayerNeuronsCount = (int)this.GetFloatAfter(lines[1], "ILNC:");
            _definition.HiddenLayersNeuronsCount = (int)this.GetFloatAfter(lines[2], "HLNC:");
            _definition.OutputLayerNeuronsCount = (int)this.GetFloatAfter(lines[3], "OLNC:");
            this.CreateLayers();

            int i;
            for (i = 5; lines[i] != "HL[0]:"; i++)
            {
                int sIndex;
                int tIndex;
                float weight;
                this.GetNeuronConnectionData(lines[i], out sIndex, out tIndex, out weight);

                _inputLayer.Neurons[sIndex].Connections[tIndex].Weight = weight;
            }

            int hiddenLayerIndex = 0;
            for (i = i + 1; i < lines.Length; i++)
            {
                if (lines[i].StartsWith("HL"))
                {
                    hiddenLayerIndex++;
                    continue;
                }

                int sIndex;
                int tIndex;
                float weight;
                this.GetNeuronConnectionData(lines[i], out sIndex, out tIndex, out weight);

                _hiddenLayers[hiddenLayerIndex].Neurons[sIndex].Connections[tIndex].Weight = weight;
            }
        }

        private void GetNeuronConnectionData(string line, out int sIndex, out int tIndex, out float weight)
        {
            int startIndex = 2;
            int endIndex = 2;

            while (line[endIndex] != ']')
            {
                endIndex++;
            }

            sIndex = int.Parse(line.Substring(startIndex, endIndex - startIndex));

            startIndex = endIndex + 2;
            endIndex = startIndex;
            
            while (line[endIndex] != ']')
            {
                endIndex++;
            }

            tIndex = int.Parse(line.Substring(startIndex, endIndex - startIndex));
            startIndex = endIndex + 2;
            endIndex = line.Length;

            weight = float.Parse(line.Substring(startIndex, endIndex - startIndex));
        }

        private float GetFloatAfter(string line, string text)
        {
            int startIndex = line.IndexOf(text) + text.Length;
            string number = line.Substring(startIndex, line.Length - startIndex);

            return float.Parse(number);
        }

        #endregion

        #region Draw

        private const int GAP = 40;
        private Font _font = new Font("Segoe UI", 8);

        public void Draw(Graphics g, Rectangle rect)
        {
            this.InitializePositions(rect);
            g.SmoothingMode = SmoothingMode.HighQuality;

            int y = 0;
            int offsetY = rect.Height / (_inputLayer.Neurons.Count() + 1);

            foreach (var inputNeuron in _inputLayer.Neurons)
                inputNeuron.Draw(g);

            foreach (NeuronsLayer hiddenLayer in _hiddenLayers)
            {
                for (int i = 0; i < hiddenLayer.Neurons.Count(); i++)
                    hiddenLayer.Neurons[i].Draw(g);
            }

            foreach (var outputNeuron in _outputLayer.Neurons)
                outputNeuron.Draw(g);
        }

        private void InitializePositions(Rectangle rect)
        {
            int offset = GAP * 2;

            Action<NeuronsLayer> initialize = layer =>
                {
                    int y = 0;
                    int offsetY = rect.Height / (layer.Neurons.Count() + 1);
                    for (int i = 0; i < layer.Neurons.Count(); i++)
                    {
                        y += offsetY;
                        layer.Neurons[i].Position = new Point(offset, y - Neuron.DIAMETER / 2);
                    }
                };

            initialize(_inputLayer);
            offset += GAP * 5;

            foreach (NeuronsLayer layer in _hiddenLayers)
            {
                initialize(layer);
                offset += GAP * 5;
            }

            initialize(_outputLayer);
        }

        #endregion
    }
}
